package simpledb.record;

import static simpledb.file.Page.*;
import simpledb.tx.Transaction;

/**
 * Manages the placement and access of records on a disk block.
 * @author Edward Sciore
 */
public class RecordPage {
	public static final int RECPAGE_HEAD_SIZE = 6 * INT_SIZE;
	public static final int REC_HEAD_SIZE = 2 * INT_SIZE;
	public static final int BASE_OFFSET = 0 * INT_SIZE;
	public static final int NEXTBLK_OFFSET = 1 * INT_SIZE;
	public static final int PREVBLK_OFFSET = 2 * INT_SIZE;
	public static final int HOLEHEAD_OFFSET = 3 * INT_SIZE;
	public static final int USEHEAD_OFFSET = 4 * INT_SIZE;
	public static final int USETAIL_OFFSET = 5 * INT_SIZE;
	
	public static final int NEXTREC_OFFSET = 0 * INT_SIZE;
	public static final int PREVREC_OFFSET = 1 * INT_SIZE;
	
	private int blk;
	private TableInfo ti;
	private Transaction tx;
	private int slotsize;
	private int currentslot = -1;
	private int base;
	private int nextblk;
	private int prevblk;
	private int holehead;
	private int usehead;
	private int usetail;

	/** Creates the record manager for the specified block.
	 * The current record is set to be before the first one.
	 * @param blk a reference to the disk block
	 * @param ti the table's metadata
	 * @param tx the transaction performing the operations
	 */
	public RecordPage(int blk, TableInfo ti, Transaction tx) {
		this.blk = blk;
		this.ti = ti;
		this.tx = tx;
		tx.pin(blk);
		base = tx.getInt(blk, BASE_OFFSET);
		nextblk = tx.getInt(blk, NEXTBLK_OFFSET);
		prevblk = tx.getInt(blk, PREVBLK_OFFSET);
		holehead = tx.getInt(blk, HOLEHEAD_OFFSET);
		usehead = tx.getInt(blk, USEHEAD_OFFSET);
		usetail = tx.getInt(blk, USETAIL_OFFSET);
		slotsize = ti.recordLength() + REC_HEAD_SIZE;
	}

	/**
	 * Closes the manager, by unpinning the block.
	 */
	public void close() {
		if (blk != -1) tx.unpin(blk);
		blk = -1;
	}

	/**
	 * move to next slot in use, if it exists
	 * @return if movement succeeds
	 */
	public boolean next() {
		int nextrec;
		if(currentslot == -1)
			nextrec = usehead;
		else
			nextrec = getNextrec(currentslot);
		if(nextrec == -1) return false;
		currentslot = nextrec;
		return true;
	}

	/**
	 * Returns the integer value stored for the
	 * specified field of the current record.
	 * @param fldname the name of the field.
	 * @return the integer stored in that field
	 */
	public int getInt(String fldname) {
		int position = fieldpos(fldname);
		return getInt(position);
	}

	/**
	 * Returns the string value stored for the
	 * specified field of the current record.
	 * @param fldname the name of the field.
	 * @return the string stored in that field
	 */
	public String getString(String fldname) {
		int position = fieldpos(fldname);
		return getString(position);
	}

	/**
	 * Stores an integer at the specified field
	 * of the current record.
	 * @param fldname the name of the field
	 * @param val the integer value stored in that field
	 */
	public void setInt(String fldname, int val) {
		int position = fieldpos(fldname);
		setInt(position, val);
	}

	/**
	 * Stores a string at the specified field
	 * of the current record.
	 * @param fldname the name of the field
	 * @param val the string value stored in that field
	 */
	public void setString(String fldname, String val) {
		int position = fieldpos(fldname);
		setString(position, val);
	}

	/**
	 * Deletes the current record.
	 * Move it to the head of hole list.
	 * Current record is marked at the first record before it.
	 * Call next() to get next record in use
	 */
	public void delete() {
		int nextrec = getNextrec(currentslot);
		int prevrec = getPrevrec(currentslot);
		
		// take out of the use list
		if(prevrec == -1)
			setUseHead(nextrec);
		else
			setNextrec(prevrec, nextrec);
		if(nextrec == -1)
			setUseTail(prevrec);
		else
			setPrevrec(nextrec, prevrec);
		
		// insert into the hole list
		setNextrec(currentslot, holehead);
		setHoleHead(currentslot);
		moveToId(getPrevrec(currentslot));
	}

	/**
	 * Inserts a new, blank record somewhere in the page.
	 * Return false if there were no available slots.
	 * @return false if the insertion was not possible
	 */
	public int insert() {
		int require = 1;
		currentslot = holehead;
		if(currentslot == -1 || (require = CrossBlkNum(currentslot)) != 0 && nextblk==-1) return require;
		// pretend to fail, in order to allocate next block 
		else
			require = 0;
		
		int prevrec = usetail;
		setHoleHead(getNextrec(currentslot));
		
		setNextrec(currentslot, -1);
		setPrevrec(currentslot, prevrec);

		if(prevrec == -1)
			setUseHead(currentslot);
		else
			setNextrec(prevrec, currentslot);
		
		setUseTail(currentslot);
		return require;
	}

	/**
	 * Sets the current record to be the record having the
	 * specified ID.
	 * @param id the ID of the record within the page.
	 */
	public void moveToId(int id) {
		currentslot = id;
	}

	/**
	 * Returns the ID of the current record.
	 * @return the ID of the current record
	 */
	public int currentId() {
		return currentslot;
	}
	
	/**
	 * @return the size of remaining part of record in current block which is crossing blocks. 
	 */
	int getRemaining(){
		return base < BLOCK_SIZE ? slotsize - (BLOCK_SIZE-base)%slotsize : base - BLOCK_SIZE;
	}
	
	int block(){
		return blk;
	}

	void setNextblk(int next){
		nextblk = next;
		tx.setInt(blk, NEXTBLK_OFFSET, nextblk);
	}

	int getNextblk(){
		return nextblk;
	}

	void setPrevblk(int prev){
		prevblk = prev;
		tx.setInt(blk, PREVBLK_OFFSET, prevblk);
	}

	int getPrevblk(){
		return prevblk;
	}
	
	/**
	 * @param rec
	 * @return how many blocks has the record cross over.
	 */
	private int CrossBlkNum(int rec){
		int cross, endPos;
		for(cross = 0, endPos = getPos(rec) + slotsize; endPos > BLOCK_SIZE;
		endPos = endPos - BLOCK_SIZE + RECPAGE_HEAD_SIZE, cross++)
			;
		return cross;
	}
	
	private int getNextrec(int recnum){
		return getInt(getPos(recnum)+NEXTREC_OFFSET);
	}
	private int getPrevrec(int recnum){
		return getInt(getPos(recnum)+PREVREC_OFFSET);
	}
	private void setNextrec(int recnum, int val){
		setInt(getPos(recnum)+NEXTREC_OFFSET, val);
	}
	private void setPrevrec(int recnum, int val){
		setInt(getPos(recnum)+PREVREC_OFFSET, val);
	}
	private void setUseHead(int firstuse){
		usehead = firstuse;
		tx.setInt(blk, USEHEAD_OFFSET, usehead);
	}
	private void setUseTail(int lastuse){
		usetail = lastuse;
		tx.setInt(blk, USETAIL_OFFSET, usetail);
	}
	private void setHoleHead(int firsthole){
		holehead = firsthole;
		tx.setInt(blk, HOLEHEAD_OFFSET, holehead);
	}

	private int fieldpos(String fldname) {
		int offset = REC_HEAD_SIZE + ti.offset(fldname);
		return getPos(currentslot) + offset;
	}
	private int getPos(int recnum){
		return base + slotsize * recnum;
	}
	

	/**
	 * Set/Get the String/Integer on the record page.
	 * If the String/Integer is crossing records, assuming needed pages are there already,
	 * just write/read on/from them.
	 */
	
	private int getInt(int position){
		int res;
		int current = blk;
		int tmp;
		
		while(position >= BLOCK_SIZE){
			tmp = current;
			current = tx.getInt(current, INT_SIZE);
			if(tmp != blk) tx.unpin(tmp);
			tx.pin(current);
			position = position - BLOCK_SIZE + RECPAGE_HEAD_SIZE;
		}
		res = tx.getInt(current, position);
		if(current != blk) tx.unpin(current);
		return res;
	}
	
	private void setInt(int position, int val){
		int current = blk;
		int tmp;
		
 		while(position >= BLOCK_SIZE){
			tmp = current;
			current = tx.getInt(current, NEXTBLK_OFFSET);
			if(tmp != blk) tx.unpin(tmp);
			tx.pin(current);
			position = position - BLOCK_SIZE + RECPAGE_HEAD_SIZE;
		}
		tx.setInt(current, position, val);
		if(current != blk) tx.unpin(current);
	}
	
	private String getString(int position){
		String str = "";
		int current = blk;

		while(str == "" || str.charAt(str.length()-1) != '\0'){
			int tmp = current;
			
			if( position < BLOCK_SIZE){
				str += tx.getString(current, position);
				position = 0;
			}
			else position -= BLOCK_SIZE;
			current = tx.getInt(current, NEXTBLK_OFFSET);
			if(tmp != blk) tx.unpin(tmp);
			if(current != -1){
				tx.pin(current);
			}
			position += RECPAGE_HEAD_SIZE;
		}
		if(current != -1 && current !=blk) tx.unpin(current);
		str = str.substring(0, str.length()-1);
		return str;
	}
	
	private void setString(int position, String val){
		String buf;
		int startIndex=0;
		int current = blk;
		int tmp = current;
		int size;
		boolean ok = false;
		do{
			if(current != blk){
				tx.pin(current);
				position += RECPAGE_HEAD_SIZE;
				if(tmp != blk) tx.unpin(tmp);
			}
			if( position < BLOCK_SIZE){
				if(BLOCK_SIZE - position < val.length()+1-startIndex)
					size = BLOCK_SIZE - position;
				else{
					size = val.length()-startIndex;
					ok = true;
				}
				buf = val.substring(startIndex, startIndex+=size);
				tx.setString(current, position, buf);
				position = 0;
			}
			else position -= BLOCK_SIZE;
			tmp = current;
			current = tx.getInt(current, NEXTBLK_OFFSET);
		}while(!ok);
		if(tmp != blk) tx.unpin(tmp);
	}
}
